package edu.uw.cs.cse461.sp12.OS;

import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;

import org.json.JSONException;
import org.json.JSONObject;

import edu.uw.cs.cse461.sp12.util.TCPMessageHandler;
import edu.uw.cs.cse461.sp12.utilIP.IPFinder;

/**
 * Implements the side of RPC that receives remote invocation requests.
 * 
 * @author zahorjan
 *
 */
public class RPCService extends RPCCallable {
	// used with the android idiom Log.x, as in Log.v(TAG, "some debug log message")
	private static final String TAG="RPCService";
	private static boolean mAmShutdown = false;
	public static int unique_id;
	
	private ServerSocket mServerSocket;
	private Thread listenerThread;
	private Map<String,Map<String, RPCCallableMethod>> serviceMap;
	
	/**
	 * This method must be implemented by RPCCallable's.  
	 * "rpc" is the well-known name of this service.
	 */
	@Override
	public String servicename() {
		return "rpc";
	}
	
	/**
	 * Constructor.  Creates the Java ServerSocket and binds it to a port.
	 * If the config file specifies an rpc.serverport value, it should be bound to that port.
	 * Otherwise, you should specify port 0, meaning the operating system should choose a currently unused port.
	 * (The config file settings are available via the OS object.)
	 * <p>
	 * Once the port is created, a thread needs to be created to listen for connections on it.
	 * 
	 * @throws Exception
	 */
	RPCService() throws Exception {
		RPCService.unique_id = 1;
		serviceMap = new HashMap<String, Map<String, RPCCallableMethod>>();
		String port = (String) OS.config().get("rpc.serverport");
		if (!port.equals("")) {
			mServerSocket = new ServerSocket(Integer.parseInt(port));
		} else {
			mServerSocket = new ServerSocket(0);
		}
		
		// Set some socket options.  
		// setReuseAddress lets you reuse a server port immediately after terminating
		// an application that has used it.  (Normally that port is unavailable for a while, for reasons we'll see
		// later in the course.
		// setSoTimeout causes a thread waiting for connections to timeout, instead of waiting forever, if no connection
		// is made before the timeout interval expires.  (You don't have to use 1/2 sec. for this value - choose your own.)
		mServerSocket.setReuseAddress(true); // allow port number to be reused immediately after close of this socket
		mServerSocket.setSoTimeout(500); // well, we have to wake up every once and a while to check for program termination

		
		
		listenerThread = new Thread(new ListenerRunnable());
		listenerThread.start();
		System.out.println("RPC server listening on " + localIP() + ":" + localPort());
	}
	
	/**
	 * System is shutting down imminently.  Do any cleanup required.
	 */
	public void shutdown() {
		mAmShutdown = true;
	}
	
	/**
	 * Services and applications with RPC callable methods register them with the RPC service using this routine.
	 * Those methods are then invoked as callbacks when an remote RPC request for them arrives.
	 * @param serviceName  The name of the service.
	 * @param methodName  The external, well-known name of the service's method to call
	 * @param method The descriptor allowing invocation of the Java method implementing the call
	 * @throws Exception
	 */
	public synchronized void registerHandler(String serviceName, String methodName, RPCCallableMethod method) throws Exception {
		Map<String, RPCCallableMethod> methodMap;
		if (serviceMap.containsKey(serviceName)) {
			methodMap = serviceMap.get(serviceName);
		} else {
			methodMap = new HashMap<String, RPCCallableMethod>();
		}
		methodMap.put(methodName, method);  // Overwrites any existing method called methodname
		serviceMap.put(serviceName, methodMap);
	}
	
	/**
	 * Returns the local IP address.
	 * @return
	 * @throws UnknownHostException
	 */
	public String localIP() throws UnknownHostException {
		InetAddress hostAddr = InetAddress.getLocalHost();
		return IPFinder.getLocalIP();
		//return hostAddr.getHostAddress();
		
	}

	/**
	 * Returns the port to which the RPC ServerSocket is bound.
	 * @return
	 */
	public int localPort() {
		return mServerSocket.getLocalPort();
	}
	
	private class ListenerRunnable implements Runnable {
		private Socket clientSocket;
		
		public ListenerRunnable() {
		}
		
		public void run() {
			while (!mAmShutdown) {
				try {
					Socket clientSocket = mServerSocket.accept();
					new Thread(new WorkerRunnable(clientSocket)).start();
				} catch (SocketTimeoutException e) {
					// this is OK
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
	
	private class WorkerRunnable implements Runnable {
		private Socket clientSocket;
		
		public WorkerRunnable(Socket clientSocket) {
			this.clientSocket = clientSocket;
		}
		
		public void run() {
			
			try {
				TCPMessageHandler msgHandler = new TCPMessageHandler(clientSocket);
				
				// read/check client connect message
				JSONObject connectMsg = msgHandler.readMessageAsJSONObject();
				if (!checkConnectMsg(connectMsg)) {
					msgHandler.sendMessage(createErrorMsg("Invalid connect message", connectMsg.getInt("id")));
					return;
				}
					
				// create and write success message
				JSONObject connectResponse = new JSONObject();
				connectResponse.put("id",unique_id++); 
				connectResponse.put("host", "");
				connectResponse.put("callid", connectMsg.get("id"));
				connectResponse.put("type", "OK");
				msgHandler.sendMessage(connectResponse);
				
				// read/check invocation message
				JSONObject invocationMsg = msgHandler.readMessageAsJSONObject();
				if (!checkInvokeMsg(invocationMsg)){
					JSONObject invokeErr = createErrorMsg("Invalid invocation message", invocationMsg.getInt("id"));
					invokeErr.put("callargs", invocationMsg);
					msgHandler.sendMessage(invokeErr);
					return;
				}
				
				String serviceName = invocationMsg.getString("app");
				String methodName = invocationMsg.getString("method");
				JSONObject args = invocationMsg.getJSONObject("args");
				
				if (!serviceMap.containsKey(serviceName) 
						|| !serviceMap.get(serviceName).containsKey(methodName)){
					JSONObject invokeErr = createErrorMsg("Invalid invocation message", invocationMsg.getInt("id"));
					invokeErr.put("callargs", invocationMsg);
					msgHandler.sendMessage(invokeErr);
					return;
				}
				
				// run method
				RPCCallableMethod method = serviceMap.get(serviceName).get(methodName);
				JSONObject returnVal = method.handleCall(args);
				
				// create write return response message
				JSONObject invocationResponse = new JSONObject();
				invocationResponse.put("id",unique_id++); 
				invocationResponse.put("host", "");
				invocationResponse.put("callid", invocationMsg.get("id"));
				invocationResponse.put("value", returnVal);
				invocationResponse.put("type", "OK");
				msgHandler.sendMessage(invocationResponse);
				
			} catch (IOException e) {
				e.printStackTrace();
			} catch (JSONException e) {
				e.printStackTrace();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		
		// Checks the connect message from client for proper format
		private boolean checkConnectMsg(JSONObject msg) throws JSONException {
			if (msg == null) {
				return false;
			} else {
				return msg.length() == 4 && 
						msg.has("id") && 
						msg.has("host") && 
						msg.has("action") && msg.getString("action").equals("connect") &&
						msg.has("type") && msg.getString("type").equals("control");
			}
		}
		
		// Checks the invocation message from client for proper format
		private boolean checkInvokeMsg(JSONObject msg) throws JSONException {
			if (msg == null) {
				return false;
			} else {
				return msg.length() == 6 && 
						msg.has("id") && 
						msg.has("app") && 
						msg.has("host") && 
						msg.has("args") && 
						msg.has("method") &&
						msg.has("type") && msg.getString("type").equals("invoke");
			}
		}
		
		// Creates a JSONObject error message
		private JSONObject createErrorMsg(String msg, int callID) throws JSONException {
			JSONObject errorMsg = new JSONObject();
			errorMsg.put("id",unique_id++);
			errorMsg.put("host", "");
			errorMsg.put("callid", callID);
			errorMsg.put("type", "ERROR");
			errorMsg.put("msg", msg);
			return errorMsg;
		}
	}
}
